package ua.edu.nuos.jeetraining2012.cms.util.google.api;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.java6.auth.oauth2.FileCredentialStore;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.calendar.CalendarScopes;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.youtube.YouTubeScopes;
import org.slf4j.Logger;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.predefined.StandardGoogleApiVariables;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.auth.GoogleAuthException;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.transport.GoogleTransportException;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.Collections;
import java.util.List;

/**
 * @author Victor N. Ganichev victor.ganichev@gmail.com
 * @version 1.0 Date: 12.07.13 Time: 13:09
 * @since 1.0
 */
public class GoogleAuthorizer {

    /** Authorizer instance. */
    private static GoogleAuthorizer instance = null;

    /** Define global instance of HTTP_TRANSPORT */
    private static HttpTransport HTTP_TRANSPORT = null;

    /** Define global instance of JSON_FACTORY */
    private static JsonFactory JSON_FACTORY = new JacksonFactory();

    /** Scope */
    private java.lang.Iterable<java.lang.String> serviceScopes;

    /**
     * Define all possible scopes, what service can use.
     */
    final private static List<String> ALL_SERVICES_SCOPES =
            com.google.gwt.thirdparty.guava.common.collect.Lists.newArrayList(
                    YouTubeScopes.YOUTUBE_UPLOAD,
                    DriveScopes.DRIVE,
                    CalendarScopes.CALENDAR,
                    "https://www.googleapis.com/oauth2/v3/userinfo"
            );

    /** Define file with preset of Google API Client ID & Client secret */
    final private static String AUTH_KEY_FILE = StandardGoogleApiVariables.AUTH_KEY_FILE;

    /** Define catalog for key chains */
    final private static String KEY_CHAIN_DIR = StandardGoogleApiVariables.KEY_CHAIN_DIR;

    /** Define logger. */
    final private static Logger logger = org.slf4j.LoggerFactory.getLogger(GoogleAuthorizer.class);

    // Constructor.
    private GoogleAuthorizer() {
        try {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
        } catch (GeneralSecurityException e) {
            logger.error("Can't create transport. Got error: {}, \nTrace:\n{}", e.getMessage(), e.getStackTrace());
        } catch (IOException e) {
            logger.error("Can't create transport. Error: {}, \nTrace:\n{}", e.getMessage(), e.getStackTrace());
        }
    }

    /**
     * Set serviceAccountScope.
     *
     * @param serviceScopes      Service Scope.
     * @return                          GoogleAuthorizer.
     */
    public GoogleAuthorizer setServiceScopes(Iterable<String> serviceScopes) {
        this.serviceScopes = serviceScopes;
        return this;
    }

    /**
     * Create GoogleAuthorizer instance, Singleton.
     *
     * @return              Instance of GoogleAuthorizer.
     */
    public static GoogleAuthorizer getInstance() {
        if (instance == null) {
            instance = new GoogleAuthorizer();
        }
        return instance;
    }

    /**
     * Create GoogleAuthorizer instance, Singleton with predefined Auth scope.
     *
     * @param serviceScopes                 Authorize scope.
     * @return                              Instance of GoogleAuthorizer.
     */
    public static GoogleAuthorizer getInstance(java.lang.Iterable<java.lang.String> serviceScopes) {
        getInstance();
        instance.serviceScopes = serviceScopes;
        return instance;
    }

    /**
     * Authorize for service account.
     *
     * @return              Instance of GoogleCredential, than can be used for connection to
     *                      Google services.
     */
    public GoogleCredential authorizeService() throws GoogleTransportException, GoogleAuthException {
        GoogleCredential credential = null;

        try {
            URI uri = GoogleAuthorizer.class.getResource("/google/key.p12").toURI();

            if (!new File(uri).exists()) {
                throw new GoogleAuthException("Can't sign request. No key file found.");
            }

            if (HTTP_TRANSPORT == null) {
                throw new GoogleTransportException("Can't init transport.");
            }

            if (serviceScopes == null) {
                serviceScopes = ALL_SERVICES_SCOPES;
            }

            credential = new GoogleCredential.Builder().setTransport(HTTP_TRANSPORT)
                    .setJsonFactory(JSON_FACTORY)
                    .setServiceAccountId(StandardGoogleApiVariables.GOOGLE_USER_EMAIL)
                    .setServiceAccountScopes(serviceScopes)
                    .setServiceAccountPrivateKeyFromP12File(new File(uri))
                    .build();
        } catch (GeneralSecurityException e) {
            logger.error("Can't authorize. Error: {}. \nTrace:\n{}", e.getMessage(), e.getStackTrace());
            throw new GoogleAuthException("Can't authorize. Error: " + e.getMessage());
        } catch (IOException e) {
            logger.error("Can't authorize. Error: {}. \nTrace:\n{}", e.getMessage(), e.getStackTrace());
            throw new GoogleAuthException("Can't authorize. Error: " + e.getMessage());
        } catch (URISyntaxException e) {
            logger.error("Can't authorize. Error: {}. \nTrace:\n{}", e.getMessage(), e.getStackTrace());
            throw new GoogleAuthException("Can't authorize. Error: " + e.getMessage());
        }
        return credential;
    }

    /**
     * Authorize for End-User account..
     * Check filled file with user ID and secret. Do authorise. In case of success, save
     * session key with drive.json name.
     *
     * @return                              Credential of authorized user.
     * @throws IOException                  If {@link GoogleAuthorizer#AUTH_KEY_FILE} can't be read.
     * @throws IllegalArgumentException     If not all necessary arguments are present in file.
     */
    public Credential authorizeEndUser() throws IOException, IllegalArgumentException {
        logger.debug("authorize() - started");

        logger.debug("authorize() Trying to load secret file.");
        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY,
                new BufferedInputStream(GoogleAuthorizer.class.getResourceAsStream(AUTH_KEY_FILE)));

        if (    clientSecrets.getDetails().getClientId().startsWith("Enter ")
                ||  clientSecrets.getDetails().getClientSecret().startsWith("Enter ")) {

            logger.error("authorize() Please feel all necessary fields in google_client_secret.json file");
            logger.debug("authorize() Client credentials are empty.");
            throw new IllegalArgumentException("Client credentials can't be empty");
        }

        if (serviceScopes == null) {
            serviceScopes = ALL_SERVICES_SCOPES;
        }

        logger.debug("authorize() Setting up file credential storage.");
        FileCredentialStore credentialStore = new FileCredentialStore(
                new java.io.File(getCreditFileName()), JSON_FACTORY);

        logger.debug("authorize() Setting up authorization code flow.");
        GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY, clientSecrets,
                serviceScopes).setCredentialStore(credentialStore).build();

        logger.debug("authorize() - finished.");
        return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
    }

    /**
     * Return file name for file credential storage, depends on requested service.
     * In case, when serviceScopes doesn't set, return google.json name.
     *
     * @return                      File Name.
     */
    private String getCreditFileName() {
        logger.debug("getCreditFileName() - started.");

        if (serviceScopes == Collections.singleton(DriveScopes.DRIVE)) {
            return KEY_CHAIN_DIR + "/drive.json";
        }

        if (serviceScopes == Collections.singleton(YouTubeScopes.YOUTUBE)) {
            return KEY_CHAIN_DIR + "/youtube.json";
        }

        if (serviceScopes == Collections.singleton(CalendarScopes.CALENDAR)) {
            return KEY_CHAIN_DIR + "/calendar.json";
        }

        logger.debug("getCreditFileName() - finished.");

        return KEY_CHAIN_DIR + "/google.json";
    }
}
